iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 26
0
Modern Web

Go into Web!系列 第 26

Day26 | 使用 Docker 封裝與運行 Go 程式(二)

  • 分享至 

  • xImage
  •  

昨天我們成功地利用 Dockerfile 打包了一個 image,但是關於 image 的大小與打包的流程都可以做最佳化,今天就來修改昨天的專案,繼續的往最佳化之路邁進吧!

範例專案

昨天的專案完成如下
https://github.com/codingXiang/random_anonymous_chat/tree/docker

Makefile

這邊我們不會對 make 指令多最描述,以興趣的朋友可以看 這裡 的介紹。

透過 Makefile 可以使得開發與發布更為方便且迅速,以下在 Makefile 中編寫

變數

GO 相關指令

透過變數將 go 的相關指令進行封裝

GO_CMD=go
GO_BUILD=$(GO_CMD) build
GO_CLEAN=$(GO_CMD) clean
GO_TEST=$(GO_CMD) test
GO_GET=$(GO_CMD) get
GO_VET=$(GO_CMD) vet
GO_RUN=$(GO_CMD) run
GO_MOD_DEP=$(GO_CMD) mod download
ALL_PATH=./...

編譯檔定義

將輸出的編譯檔也定義成變數

BINARY_NAME=app
UNPACK_PATH=$$path

Docker 相關指令

docker 相關指令定義成變數

DOCKER_CMD=docker
DOCKER_BUILD=$(DOCKER_CMD) build
DOCKER_PUSH=$(DOCKER_CMD) push
DOCKER_IMAGE_NAME=random_anonymous_chat

Target

通過不同的 target 實現系列的步驟

Go

將 go 相關情境封裝成 target

deps:
	$(GO_MOD_DEP)
test:
	$(GO_TEST) -v $(ALL_PATH) -cover
build:
	$(GO_BUILD) -o $(BINARY_NAME)
run:
	$(GO_RUN) main.go
clean:
	$(GO_CLEAN)
	rm -f $(BINARY_NAME)
pack:
	tar -cvzf $(BINARY_NAME)-v$(VERSION).tar.gz $(BINARY_NAME) ./config ./template
unpack:
	tar -zxf $(BINARY_NAME).tar.gz -C $(UNPACK_PATH)

docker

將 docker 相關情境封裝成 target

docker_build:
	@echo "開始打包 Docker Image - $(DOCKER_FULL_IMAGE)"
	$(DOCKER_BUILD) -t $(DOCKER_IMAGE_NAME) .
docker_push:
	@echo "開始 push docker image - $(DOCKER_FULL_IMAGE)"
	$(DOCKER_PUSH) $(DOCKER_IMAGE_NAME)

總結

完整的 makefile 如下

GO_CMD=go
GO_BUILD=$(GO_CMD) build
GO_CLEAN=$(GO_CMD) clean
GO_TEST=$(GO_CMD) test
GO_GET=$(GO_CMD) get
GO_VET=$(GO_CMD) vet
GO_RUN=$(GO_CMD) run
GO_MOD_DEP=$(GO_CMD) mod download
ALL_PATH=./...

BINARY_NAME=app
UNPACK_PATH=$$path

DOCKER_CMD=docker
DOCKER_BUILD=$(DOCKER_CMD) build
DOCKER_PUSH=$(DOCKER_CMD) push
DOCKER_IMAGE_NAME=random_anonymous_chat

deps:
	$(GO_MOD_DEP)
test:
	$(GO_TEST) -v $(ALL_PATH) -cover
build:
	$(GO_BUILD) -o $(BINARY_NAME)
run:
	$(GO_RUN) main.go
clean:
	$(GO_CLEAN)
	rm -f $(BINARY_NAME)
pack:
	tar -cvzf $(BINARY_NAME)-v$(VERSION).tar.gz $(BINARY_NAME) ./config ./template
unpack:
	tar -zxf $(BINARY_NAME).tar.gz -C $(UNPACK_PATH)
docker_build:
	@echo "開始打包 Docker Image - $(DOCKER_FULL_IMAGE)"
	$(DOCKER_BUILD) -t $(DOCKER_IMAGE_NAME) .
docker_push:
	@echo "開始 push docker image - $(DOCKER_FULL_IMAGE)"
	$(DOCKER_PUSH) $(DOCKER_IMAGE_NAME)

測試

可以直接通過 make docker_build 進行打包

修改原本的 Dockerfile

原先的 dockerfile 有幾個地方可以進行修改,分別為:

  1. 將 FROM 的 base image 縮小
  2. 透過 muti-stage 的機制將包完的檔案放置另一個 image 內

以下針對這兩個步驟一一說明

將 FROM 的 base image 縮小

一開始我們選用的是 完整的 golang image,但是在打包的時候我們可以選擇 alpine 版本的,這樣的 image 會小很多,因為 alpinelinux 推出的極小 OS。

直接將 FROM 的部分改為 codingxiang/go_vc

FROM  golang:alpine

透過 muti-stage 的機制將包完的檔案放置另一個 image 內

關於 muti-stage 的部分有興趣的朋友可以參考 這裡

由於 Go 在編譯後就是一個 binary 的執行檔,也就是可以不用有 go 的環境也可以執行,因此透過 mulit-stage 的方式來打包是一個不錯的選擇,這邊我們將整個流程分成 3 個 stage:

Stage1 : 用來 download 相依套件
Stage2 : 用來 build binary
Stage3 : 真正可執行的 Docker Image

就讓我們一個一個來吧

Stage1

Stage1 的部分主要是用來下載相依套件,而相依套件是透過 go.modgo.sum 兩個檔案所決定要下載哪些,因此在此 stage 只要將這兩個檔案複製進來即可
[備註] FROM 後面的 AS 是用來定義此 stage 的內容,其他的 stage 可以透過此識別碼來取得裡面的內容

FROM    codingxiang/go_vc AS stage1
ENV     RUN_PATH=/app PROJ_PATH=/build
RUN     mkdir -p $RUN_PATH
WORKDIR $RUN_PATH
ENV     GO111MODULE=on
COPY    go.mod .
COPY    go.sum .
RUN     go mod download

Stage2

Stage2 的部分主要用來編譯,因此要先繼承 stage1 的內容

FROM    stage1 AS stage2

接著設定角色為 root

USER    root

接著將外部的其他檔案 COPY 到 Container 內並且設定 WORKDIR 為 $PROJ_PATH

ADD     . $PROJ_PATH
WORKDIR $PROJ_PATH

因為有寫好 makefile,所以只要呼叫 Makefile 內的 target 即可,這邊有三個步驟要執行,依序為

  1. 打包
  2. 封裝成 tar
  3. 解壓縮 tar 到特定目錄
    因此可以寫成以下
RUN     make build pack unpack path=$RUN_PATH

整個 Stage2 的語法如下:

FROM    stage1 AS stage2
USER    root
ADD     . $PROJ_PATH
WORKDIR $PROJ_PATH
RUN     make build pack unpack path=$RUN_PATH

Stage3

最後要來將編譯好的檔案放置到完全空的 image 中執行,因為這樣可以省去非常多的空間。

FROM 的部分選用 alpine 且 USER 選用 root,這個之前提到過,是目前最小的 OS Image 了

FROM    alpine
USER    root

定義執行路徑為環境變數並且同時建立,執行路徑為 /app

ENV     RUN_PATH=/app
RUN     mkdir -p $RUN_PATH

複製 stage2 編號好的檔案至此

COPY    --from=stage2 ${RUN_PATH} ${RUN_PATH}

最後定義執行路徑為 /app,程式進入點為 ./app 這個 binary

WORKDIR ${RUN_PATH}
ENTRYPOINT ["./app"]

整個 Stage3 的語法如下:

FROM    alpine
USER    root
ENV     RUN_PATH=/app
RUN     mkdir -p $RUN_PATH
COPY    --from=stage2 ${RUN_PATH} ${RUN_PATH}
WORKDIR ${RUN_PATH}
ENTRYPOINT ["./app"]

測試

執行 make docker_build 的指令,會獲得結果如下

Sending build context to Docker daemon  286.7kB
Step 1/20 : FROM    codingxiang/go_vc AS stage1
 ---> f3e1d12b7aff
Step 2/20 : ENV     RUN_PATH=/app PROJ_PATH=/build
 ---> Using cache
 ---> 63318e54be20
Step 3/20 : RUN     mkdir -p $RUN_PATH
 ---> Using cache
 ---> b03acc57efea
Step 4/20 : WORKDIR $RUN_PATH
 ---> Using cache
 ---> 9f53c01fb72b
Step 5/20 : ENV     GO111MODULE=on
 ---> Using cache
 ---> d4d3b5c6b45b
Step 6/20 : COPY    go.mod .
 ---> Using cache
 ---> edf9008a4cb1
Step 7/20 : COPY    go.sum .
 ---> Using cache
 ---> 5ba7a0b07900
Step 8/20 : RUN     go mod download
 ---> Using cache
 ---> 718c688752f8
Step 9/20 : FROM    stage1 AS stage2
 ---> 718c688752f8
Step 10/20 : USER    root
 ---> Using cache
 ---> e55e5b803080
Step 11/20 : ADD     . $PROJ_PATH
 ---> Using cache
 ---> 5a7ac1599142
Step 12/20 : WORKDIR $PROJ_PATH
 ---> Using cache
 ---> b32bcb01f9e9
Step 13/20 : RUN     make build pack unpack path=$RUN_PATH
 ---> Using cache
 ---> 58cfaef333ed
Step 14/20 : FROM    alpine
 ---> a24bb4013296
Step 15/20 : USER    root
 ---> Using cache
 ---> e55d31193b1c
Step 16/20 : ENV     RUN_PATH=/app
 ---> Using cache
 ---> c3052518d3f6
Step 17/20 : RUN     mkdir -p $RUN_PATH
 ---> Using cache
 ---> 887503deae49
Step 18/20 : COPY    --from=stage2 ${RUN_PATH} ${RUN_PATH}
 ---> Using cache
 ---> d80087020e62
Step 19/20 : WORKDIR ${RUN_PATH}
 ---> Using cache
 ---> 6ebd7a6b6288
Step 20/20 : ENTRYPOINT ["./app"]
 ---> Using cache
 ---> 463359743e6b
Successfully built 463359743e6b
Successfully tagged random_anonymous_chat:latest

因為我已經執行過了,所以可以看到每一層 layer 都是寫 Using cache

可以透過 docker images 來查看目前 build 的 image 大小

REPOSITORY                                           TAG                                              IMAGE ID            CREATED             SIZE
random_anonymous_chat                                latest                                           463359743e6b        16 minutes ago      27.5MB

小結

從結果可以發現,透過今天這些流程的最佳化,將原先 1.2G 大小的 Image 縮小至 27.5MB,整個縮小了許多,也因為使用了 multi-stage 的機制,所以如果沒有新的相依套件的話,在 build stage1 時就會使用上次的 cache,整體的速度也加快許多。

從明天開始我們花點時間說一下關於 CI 要怎麼做吧!


上一篇
Day 25 | 使用 Docker 封裝與運行 Go 程式(一)
下一篇
Day 27 | CI/CD 的導入 - 概念篇
系列文
Go into Web!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
leooooooo
iT邦新手 5 級 ‧ 2021-11-22 10:07:29

你好
docker build有一個問題需要請教一下,
原本程式碼未使用Go 1.17後的func,是可以正常build的。
但是使用到了Go 1.17後的func ,例如:time.IsDST,就會出現undefined。

程式直接go run 是正常運作的。

 => CACHED [stage1 6/7] COPY    go.sum .
 => CACHED [stage1 7/7] RUN     go mod download
 => CACHED [stage2 1/3] ADD     . /build
 => CACHED [stage2 2/3] WORKDIR /build
 => ERROR [stage2 3/3] RUN     make build pack unpack path=/app  
------
 > [stage2 3/3] RUN     make build pack unpack path=/app:
#20 0.216 go build -o app
#20 1.730 # godemo
#20 1.730 ./main.go:18:18: date.IsDST undefined (type "time".Time has no field or method IsDST)
#20 1.730 note: module requires Go 1.17
#20 1.759 make: *** [Makefile:24: build] Error 2
------
executor failed running [/bin/sh -c make build pack unpack path=$RUN_PATH]: exit code: 2

請問有甚麼方式可以解決?

阿翔 iT邦新手 4 級 ‧ 2021-11-24 14:52:45 檢舉

會發生這個錯誤的原因在於這個範例中 Dockerfile stage1FROMcodingxiang/go_vc,這個 image 我當初包的版本是 1.14,這邊你可以把 image 換成 golang:1.17.0-alpine 試試看~

leooooooo iT邦新手 5 級 ‧ 2021-11-24 16:23:51 檢舉

已成功Build了,感謝。

補充說明:使用golang:< version >-alpine,
需安裝make,才可正常使用make指令。

RUN     apk update && apk add make

若有使用到git指令則需安裝git

RUN     apk update && apk add git
0
小克
iT邦新手 4 級 ‧ 2023-02-02 12:27:01

針對 Makefile 的部分
我也寫了一篇分享文
歡迎交流!
https://blog.goodjack.tw/2023/01/use-makefile-to-manage-workflows-for-web-projects.html

我要留言

立即登入留言